/*
 * Created on Oct 27, 2004
 */
package no.ntnu.fp.net.co;

import java.io.IOException;
import java.net.ConnectException;
import java.net.InetAddress;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import no.ntnu.fp.net.cl.ClException;
import no.ntnu.fp.net.cl.ClSocket;
import no.ntnu.fp.net.cl.KtnDatagram;
import no.ntnu.fp.net.cl.KtnDatagram.Flag;

/**
 * Implementation of the Connection-interface. <br>
 * <br>
 * This class implements the behaviour in the methods specified in the interface
 * {@link Connection} over the unreliable, connectionless network realised in
 * {@link ClSocket}. The base class, {@link AbstractConnection} implements some
 * of the functionality, leaving message passing and error handling to this
 * implementation.
 * 
 * @author Sebj�rn Birkeland and Stein Jakob Nordb�
 * @see no.ntnu.fp.net.co.Connection
 * @see no.ntnu.fp.net.cl.ClSocket
 */
public class ConnectionImpl extends AbstractConnection {

	// /** Number of times to wait for acks */
	// private static final int TRIES = 3;

	/** Keeps track of the used ports for each server port. */
	private static Map<Integer, Boolean> usedPorts = Collections
			.synchronizedMap(new HashMap<Integer, Boolean>());

	/**
	 * Initialize initial sequence number and setup state machine.
	 * 
	 * @param myPort
	 *            - the local port to associate with this connection
	 */
	public ConnectionImpl(int myPort) {
		super();
		this.myPort = myPort;
		this.myAddress = getIPv4Address();
	}

	public ConnectionImpl() {
		super();

		this.myPort = nextPort();
		this.myAddress = getIPv4Address();

	}

	public String getIPv4Address() {
		try {
			return InetAddress.getLocalHost().getHostAddress();
		} catch (UnknownHostException e) {
			return "127.0.0.1";
		}
	}

	public boolean isEstablished() {
		return (state == State.ESTABLISHED);
	}

	/**
	 * Gets a randomly assigned free port
	 * 
	 * @return free port
	 */
	private int nextPort() {
		int newPort;
		do {
			newPort = (int) (10000 + Math.random() * 11000);
		} while (usedPorts.get(newPort) != null
				&& usedPorts.get(newPort) == true);

		usedPorts.put(newPort, true);
		return newPort;
	}

	/**
	 * Help method for sending packets.
	 * 
	 * @param {@link KtnDatagram} - packet to be sent
	 * @return {@link KtnDatagram} - ack packet received
	 * @throws ConnectException
	 * @throws IOException
	 *             - if send fails
	 */
	private KtnDatagram internalSender(KtnDatagram packet)
			throws ConnectException, IOException {
		KtnDatagram ret = null;
		int retry = 20;
		// prøver å sende opptil 20 ganger
		while (!isValid(ret) && retry > 0) {
			retry--;
			try {
				simplySendPacket(packet);
				ret = receiveAck();
			} catch (ClException e) {
				e.printStackTrace();
			}
		}

		System.out.println("-------------------------------------x"
				+ ret.getFlag());
		if (!isValid(ret)
				|| !((ret.getFlag() == Flag.ACK)
						|| (ret.getFlag() == Flag.SYN_ACK && state == State.SYN_SENT) || ret
						.getFlag() == Flag.FIN)) {
			state = State.CLOSED;
			System.out.println("Send Failed");
			throw new IOException("Send failed");
		}
		return ret;
	}

	/**
	 * Establish a connection to a remote location.
	 * 
	 * @param remoteAddress
	 *            - the remote IP-address to connect to
	 * @param remotePort
	 *            - the remote portnumber to connect to
	 * @throws IOException
	 *             If there's an I/O error.
	 * @throws java.net.SocketTimeoutException
	 *             If timeout expires before connection is completed.
	 * @see Connection#connect(InetAddress, int)
	 */
	public void connect(InetAddress remoteAddress, int remotePort)
			throws IOException, SocketTimeoutException {
		connect(remoteAddress.getHostAddress(), remotePort);
	}

	/**
	 * Overloaded method for String argument for ip
	 * 
	 * @param {@link String} remoteAddress
	 * @param int remotePort
	 * 
	 * @see ConnectionImpl#connect(InetAddress, int)
	 * @see Connection#connect(String, int)
	 */
	public void connect(String remoteAddress, int remotePort)
			throws IOException, SocketTimeoutException {

		if (state != State.CLOSED) { // kan ikke connecte dersom ikke i CLOSED
			throw new IOException("Not Closed socket");
		}

		this.remoteAddress = remoteAddress;
		this.remotePort = remotePort;

		KtnDatagram syn = constructInternalPacket(Flag.SYN);

		try {
			state = State.SYN_SENT;
			// sender pakke med intern sender
			lastValidPacketReceived = internalSender(syn);
			// setter port til den mottparten vil ha
			this.remotePort = lastValidPacketReceived.getSrc_port();
			sendAck(lastValidPacketReceived, false);
			System.out.println("syn sent");
			state = State.ESTABLISHED;
		} catch (Exception e) {
			state = State.CLOSED;
			throw new IOException("Error while connecting");
		}
	}

	/**
	 * Sets up the received connection for accept
	 * 
	 * @param {@link KtnDatagram} synpack - the received SYN-packet
	 * @return {@link Connection} this
	 * @throws {@link IOException} - if failed to setup connection
	 */
	private Connection setupConnection(KtnDatagram synpack) throws IOException {
		lastValidPacketReceived = synpack;
		state = State.SYN_RCVD;

		remoteAddress = synpack.getSrc_addr();
		remotePort = synpack.getSrc_port();

		// sender SYN_ACK
		sendAck(synpack, true);
		// skal få ACK
		KtnDatagram ret = receiveAck();
		if (!isValid(ret)) {
			state = State.CLOSED;
			throw new IOException("Failed to setup connection");
		}
		state = State.ESTABLISHED;
		System.out.println("ESTABLISHED incomming");

		return this;
	}

	/**
	 * Listen for, and accept, incoming connections.
	 * 
	 * @return A new ConnectionImpl-object representing the new connection.
	 * @see Connection#accept()
	 */
	public Connection accept() throws IOException, SocketTimeoutException {
		if (state != State.CLOSED && state != State.LISTEN) {
			throw new IOException("Wrong state");
		}

		KtnDatagram synrec = null;
		state = State.LISTEN;
		while (!isValid(synrec) || synrec.getFlag() != Flag.SYN) {
			synrec = receivePacket(true);
		}

		// lager en ny connection for applikasjonen å bruke
		ConnectionImpl con = new ConnectionImpl(nextPort());
		return con.setupConnection(synrec);
	}

	/**
	 * Send a message from the application.
	 * 
	 * @param msg
	 *            - the String to be sent.
	 * @throws ConnectException
	 *             If no connection exists.
	 * @throws IOException
	 *             If no ACK was received.
	 * @see AbstractConnection#sendDataPacketWithRetransmit(KtnDatagram)
	 * @see no.ntnu.fp.net.co.Connection#send(String)
	 */
	public void send(String msg) throws ConnectException, IOException {
		if (state != State.ESTABLISHED) {
			throw new ConnectException("No connection is established");
		}
		// sender pakken
		KtnDatagram pack = constructDataPacket(msg);
		KtnDatagram ack = sendDataPacketWithRetransmit(pack);
		if (ack == null) {
			throw new IOException("no ack received for the packet");
		}
		if (ack.getAck() < pack.getSeq_nr()
				|| ack.getAck() > (pack.getSeq_nr() + 10)) {
			throw new IOException("wrong ack for the packet");
		}
		System.out.println("packetSent");
	}

	/**
	 * Wait for incoming data.
	 * 
	 * @return The received data's payload as a String.
	 * @see Connection#receive()
	 * @see AbstractConnection#receivePacket(boolean)
	 * @see AbstractConnection#sendAck(KtnDatagram, boolean)
	 */
	public String receive() throws ConnectException, IOException {
		// throw new NotImplementedException();

		if (state != State.ESTABLISHED) {
			throw new ConnectException(
					"Connection has not yet been established");
		}

		// vent til vi får noe
		KtnDatagram packet = receivePacket(false);

		// sjekk det vi får
		if (!isValid(packet)) {
			sendAck(lastValidPacketReceived, false);
			return null;
		}

		// ack om det er noe fornuftig
		sendAck(packet, false);
		lastValidPacketReceived = packet;
		return packet.toString();

	}

	/**
	 * Close the connection.
	 * 
	 * @see Connection#close()
	 * 
	 * @param true if the FIN was received, false if the close was initiated
	 */
	public void close() throws IOException {
		try {
			if (state != State.ESTABLISHED) {
				throw new ConnectException(
						"Connection has not yet been established");
			}

			state = State.FIN_WAIT_1;

			KtnDatagram ret = null;
			KtnDatagram fin = constructInternalPacket(Flag.FIN);

			// prøver 4 ganger
			int retry = 5;
			System.out.println("-------------------------------------1");
			while ((ret == null || ret.getFlag() == Flag.FIN) && --retry > 0) {

				System.out.println("-------------------------------------2");
				// dersom vi fikk FIN først
				if (disconnectRequest != null) {

					System.out
							.println("-------------------------------------3");
					sendAck(disconnectRequest, false);
					state = State.FIN_WAIT_2;
				}
				ret = internalSender(fin);
				System.out.println("------------------------------------- "
						+ ret + " " + ret.getFlag());
				if (ret.getFlag() == Flag.FIN) {

					System.out
							.println("-------------------------------------4");
					disconnectRequest = ret;
				}
			}

			System.out.println("-------------------------------------5");
			if (state == State.FIN_WAIT_1 && retry > 0) {
				state = State.CLOSE_WAIT;
				retry = 5;
				while (!isValid(disconnectRequest) && --retry > 0) {
					disconnectRequest = receivePacket(true);
				}
				sendAck(disconnectRequest, false);
			}
		} catch (Exception e) {
			state = State.CLOSED;
			throw new IOException("NOE GIKK GALT!, i close()" + e);
		}
		state = State.CLOSED;
	}

	/**
	 * Test a packet for transmission errors. This function should only called
	 * with data or ACK packets in the ESTABLISHED state.
	 * 
	 * @param packet
	 *            Packet to test.
	 * @return true if packet is free of errors, false otherwise.
	 */
	protected boolean isValid(KtnDatagram packet) {
		// null pakke er ikke bra
		if (packet == null) {
			System.out.println("packet is null, time: "
					+ System.currentTimeMillis());
			return false;
		}
		if (state == State.FIN_WAIT_1 || state == State.FIN_WAIT_2)
			return true;
		// feil på pakken er ikke bra
		if (packet.calculateChecksum() != packet.getChecksum()) {
			System.out.println("corrupt packet");
			return false;
		}
		// HAX for connect
		if (lastValidPacketReceived == null)
			return true;
		// sjekk at sekvensnummeret er riktig, til en viss grad
		if (packet.getSeq_nr() <= lastValidPacketReceived.getSeq_nr()
				|| packet.getSeq_nr() > lastValidPacketReceived.getSeq_nr() + 10) {
			System.out.println("invalid seq_nr");
			return false;
		}
		return true;
	}

}
